TypeScript 50.6%
JavaScript 46.1%
SQL 2.1%
Shell 0.6%
CSS 0.5%
1import { NextRequest, NextResponse } from "next/server";2import { z } from "zod";3import { db } from "@/db/client";4import { listingMedia } from "@/db/schema";5import { assertSameOrigin, requireUser } from "@/lib/auth/session";6import { getListingFull, assertOwner } from "@/lib/listings/service";7import { countMedia, MAX_PER_LISTING, MAX_BYTES, newUploadToken } from "@/lib/listings/media";8import { apiError, handle, parseBody } from "@/lib/api";9export const dynamic = "force-dynamic";10const Body = z.object({ fileName: z.string().max(200).optional(), mimeType: z.string().max(80).optional(), bytes: z.number().int().positive().max(MAX_BYTES).optional() });11/** Équivalent local d'une URL pré-signée : jeton à usage unique, lié à l'annonce et au propriétaire ; l'upload se fait ensuite en PUT sur /api/uploads/<token>. */12export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {13 try {14 await assertSameOrigin(); const u = await requireUser(); const { id } = await params; const b = await parseBody(req, Body);15 const l = await getListingFull(id); if (!l) return apiError(404, "not_found", "Annonce introuvable"); assertOwner(l, u.id, u.roles);16 if ((await countMedia(id)) >= MAX_PER_LISTING) return apiError(422, "too_many", `Maximum ${MAX_PER_LISTING} photos par annonce`);17 const token = newUploadToken();18 const [m] = await db().insert(listingMedia).values({ listingId: id, storageKey: `pending/${token}`, mimeType: b.mimeType ?? null, position: l.media.length, status: "UPLOADING", uploadToken: token }).returning({ id: listingMedia.id });19 return NextResponse.json({ mediaId: m.id, uploadUrl: `/api/uploads/${token}`, method: "PUT", maxBytes: MAX_BYTES, expiresInSec: 900 });20 } catch (e) { return handle(e); }21}22